package system.agent;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;

import system.DebugOptionInterface;
import system.util.Loader;
import system.util.Server;
import system.util.message.Message;


/* The agent class. This is an abstract class that any agent class
   should extend. It declares the "main" method, but does not
   implement it. The agent class must provide an implementation
   for this method. Notice that inheriting the "agent" class makes
   the agent implements the Serializable interface. Notice also
   that any other class used by the agent must implements the
   Serializable interface.

   Refer to agent_itf for explanation about the methods
 */

public abstract class Agent implements AgentInterface, DebugOptionInterface {

	private static final long serialVersionUID = -7803011636116546816L;

	/* The name of the agent (the name of its main class) */
	private String agentName;

	/* The host and port where the agent has been created */
	private String initialHost;
	private int initialPort;

	// the current host and port
	private String currentHost;
	private int currentPort;

	/**
	 * Semaphore to synchronize the management of the letter box
	 */
	private transient Object synchroLetterBox = new Object();

	/**
	 * Letter box which store incoming messages from others agents
	 */
	private transient List<Message> letterBox;

	public Agent(String agentName, String host, int port) {
		this.agentName = agentName;
		if (host.equals("localhost")) {
			try {
				this.initialHost = InetAddress.getLocalHost().getHostName();
			} catch (UnknownHostException e) {
				e.printStackTrace();
			};
		}
		else {
			this.initialHost = host;
		}
		this.initialPort = port;
		this.currentHost = this.initialHost;
		this.currentPort = this.initialPort;
		this.letterBox = new ArrayList<Message>();
		Server.registerAgent(agentName, this);
	}

	@Override
	public String getAgentName() {
		return this.agentName;
	}

	public String getInitialHost() {
		return initialHost;
	}

	public int getInitialPort() {
		return initialPort;
	}

	public String getCurrentHost() {
		return currentHost;
	}

	public int getCurrentPort() {
		return currentPort;
	}

	/**
	 * Retrieve and remove the next message of the letterBox
	 * Return the next message of the letterBox or null if the letterBox is empty
	 */
	public Message getNextMessageInLetterBox(){
		synchronized(synchroLetterBox){
			if(letterBox.size() > 0)
				return letterBox.remove(0);
			else return null;
		}
	}

	/**
	 * Retrieve and remove the next message of the letterBox, if there
	 * are no message, wait until a message arrive.
	 * @return the next message of the letterBox
	 */
	public Message waitForNextMessage(){
		try {
			synchronized(synchroLetterBox){
				while(letterBox.size() == 0)
					synchroLetterBox.wait();
				return letterBox.remove(0);
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
			return null;
		}

	}

	/* The migration primitive */
	@SuppressWarnings("deprecation")
	public void move(String host, int port) {
		String oldHost = this.currentHost;
		int oldPort = this.currentPort;
		try {
			if (OPTION) { 
				System.out.println("Migration de l'agent " + agentName + " de " + currentHost + ":" + currentPort + " vers " + host + ":" + port);
			}

			this.currentHost = host;
			this.currentPort = port;
			Server.unregister(agentName);

			/* Open a connection to the server */
			Socket s = new Socket (host, port);

			/* Create an object stream to the server */
			OutputStream os = s.getOutputStream();
			ObjectOutputStream oos = new ObjectOutputStream(os);

			//Extract the agent code from the class loader associated with the agent and send it to the server.
			//Loader l = (Loader)this.getClass().getClassLoader();
			Loader l = new Loader(this.getClass().getName());
			oos.writeObject(l.extractCode());

			/* Send the agent's data to the server.
			   Notice that the code must be sent before the data in order to allow deserializing the data.
			 */
			oos.writeObject(this);

			//Clean up
			s.close();

			Thread.currentThread().stop();
		} catch (Exception ex) {
			//we "rollback" ths host and port
			this.currentHost = oldHost;
			this.currentPort = oldPort;
			System.out.println("Agent(move) error: " + ex.getMessage());
			ex.printStackTrace();
		}
	}

	protected void back() {
		/* Back home */
		move(initialHost, initialPort);
	}

	@Override
	public void handleMessage(Message message){
		synchronized (synchroLetterBox) {
			letterBox.add(message);		
			synchroLetterBox.notifyAll();
		}
	}

	@Override
	public void sendMessage(String to, Message message){
		Server.getAgent(to).handleMessage(message);
	}

	private void readObject(ObjectInputStream out) throws IOException{
		try {
			out.defaultReadObject();
			letterBox = new ArrayList<Message>();
			synchroLetterBox = new Object();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}
}



